Contents

2D simulation of CPWC sequence with kWave

This example demonstrates the use of k-Wave for simulation of ultrasonic imaging modalities and how to beamform it with USTB. The k-Wave Toolbox (http://www.k-wave.org) should be in MATLAB's path.

authors: Alfonso Rodriguez-Molares alfonso.r.molares@ntnu.no

Based on code by Bradley Treeby k-Wave Toolbox (http://www.k-wave.org) Copyright (C) 2009-2017 Bradley Treeby

Last updated: 30.10.2018

close all;

Basic definitions

We define some constants to be used on the script

f0 = 2e6;       % pulse center frequency [Hz]
cycles=2;       % number of cycles in pulse
c0 = 1540;      % medium speed of sound [m/s]
rho0 = 1020;    % medium density [kg/m3]
F_number = 1.7; % F number for CPWC sequence (i.e. maximum angle)
N=1;            % number of plane waves in CPWC sequence

uff.probe

We define the ultrasound probe as a USTB structure.

prb=uff.linear_array();
prb.N=128;                  % number of elements
prb.pitch=300e-6;           % probe pitch in azimuth [m]
prb.element_width=300e-6;   % element width [m]
prb.element_height=5000e-6; % element height [m]
fig_handle = prb.plot([],'Linear array');

Computational grid

We can define the computational grid as a uff.linear_scan strcuture. We set different resolution options depending on frequency reference speed of sound.

f_max = 1.2*f0;
lambda_min = c0/f_max;

% mesh resolution, choose one
mesh_resolution='element2';
switch mesh_resolution
    case 'element2' % around 50 sec per wave
        dx=prb.pitch/2;                                         % 2 elements per pitch
    case 'element4' % around 6min sec per wave
        dx=prb.pitch/4;                                         % 2 elements per pitch
    otherwise
        error('Not a valid option');
end

% mesh size
PML_size = 20;                                          % size of the PML in grid points
Nx=round(40e-3/dx); Nx=Nx+mod(Nx,2);
Nz=round(40e-3/dx); Nz=Nz+mod(Nz,2);
grid_width=Nx*dx;
grid_depth=Nz*dx;
domain=uff.linear_scan('x_axis', linspace(-grid_width/2,grid_width/2,Nx).', 'z_axis', linspace(0,grid_depth,Nz).');

kgrid = kWaveGrid(domain.N_z_axis, domain.z_step, domain.N_x_axis, domain.x_step);

Propagation medium

We define the medium based by setting the sound speed and density in every pixel of the uff.scan. Here we set an hyperechoic cyst at the center of the domain.

% transparent background
medium.sound_speed = c0*ones(domain.N_z_axis, domain.N_x_axis);   % sound speed [m/s]
medium.density = rho0.*ones(domain.N_z_axis, domain.N_x_axis);      % density [kg/m3]

% include hyperechoic cyst
cyst_std = 3/100;
cx=0; cz=20e-3; cr = 5e-3;
cn=sqrt((domain.x-cx).^2+(domain.z-cz).^2)<cr;
medium.sound_speed(cn) = random('normal',1540,1540*cyst_std,size(medium.sound_speed(cn)));       % sound speed [m/s]
medium.density(cn) = random('normal',1020,1020*cyst_std,size(medium.density(cn)));               % density [kg/m3]

% attenuation
medium.alpha_coeff = 0.3;  % [dB/(MHz^y cm)]
medium.alpha_power = 1.5;

% show physical map: speed of sound and density
figure;
subplot(1,2,1);
imagesc(domain.x_axis*1e3,domain.z_axis*1e3,medium.sound_speed); colormap gray; colorbar; axis equal tight;
xlabel('x [mm]');
ylabel('z [mm]');
title('c_0 [m/s]');
subplot(1,2,2);
imagesc(domain.x_axis*1e3,domain.z_axis*1e3,medium.density); colormap gray; colorbar; axis equal tight;
xlabel('x [mm]');
ylabel('z [mm]');
title('\rho [kg/m^3]');

Time vector

We define the time vector depending on the CFL number, the size of the domain and the mean speed of sound.

cfl=0.3;
t_end=2*sqrt(grid_depth.^2+grid_depth.^2)/mean(medium.sound_speed(:));
kgrid.makeTime(medium.sound_speed,cfl,t_end);

Sequence

We define a sequence of plane-waves

alpha_max=1/2/F_number;                         % maximum angle span [rad]
if N>1
    angles=linspace(-alpha_max,alpha_max,N);    % angle vector [rad]
else
    angles = 0;
end
seq=uff.wave();
for n=1:N
    seq(n)=uff.wave();
    seq(n).apodization = uff.apodization('f_number',1,'window',uff.window.rectangular,'focus',uff.scan('xyz',[0 0 10e-3]));
    seq(n).source.azimuth=angles(n);
    seq(n).source.distance=-Inf;
    seq(n).probe=prb;
    seq(n).sound_speed=1540;    % reference speed of sound [m/s]
    seq(n).delay = min(seq(n).delay_values);
    seq(n).source.plot(fig_handle);
end

Source & sensor mask

Based on the uff.probe we find the pixels in the domain that must work as source and sensors.

% find the grid-points that match the element
source_pixels={};
element_sensor_index = {};
n=1;
for m=1:prb.N_elements
    plot((prb.x(m)+[-prb.width(m)/2 prb.width(m)/2])*1e3,[0 0],'k+-'); hold on; grid on;
    source_pixels{m}=find(abs(domain.x-prb.x(m))<prb.width(m)/2 & abs(domain.y-prb.y(m))<prb.height(m) & abs(domain.z-prb.z(m))<=domain.z_step/2);
    element_sensor_index{m} = n:n+numel(source_pixels{m})-1;
    n=n+numel(source_pixels{m});
end

% sensor mask
sensor.mask = zeros(domain.N_z_axis, domain.N_x_axis);
for m=1:prb.N_elements
    sensor.mask(source_pixels{m}) = sensor.mask(source_pixels{m}) + 1;
end

% source mask
source.u_mask=sensor.mask;

figure;
h=pcolor(domain.x_axis,domain.z_axis,source.u_mask); axis equal tight;
title('Source/Sensor mask')
set(h,'edgecolor','none');
set(gca,'YDir','reverse');
xlabel('x [mm]');
ylabel('z [mm]');

Calculation

We are ready to launch the k-Wave calculation

disp('Launching kWave. This can take a while.');
for n=1:N
    delay=seq(n).delay_values-seq(n).delay;
    denay=round(delay/kgrid.dt);
    seq(n).delay = seq(n).delay - cycles/f0/2;

    % offsets
    tone_burst_offset = [];
    for m=1:prb.N_elements
        tone_burst_offset = [tone_burst_offset repmat(denay(m),1,numel(source_pixels{m}))];
    end
    source.ux = toneBurst(1/kgrid.dt, f0, cycles, 'SignalOffset', tone_burst_offset);   % create the tone burst signals
    source.uy = 0.*source.ux;
    source.u_mode ='dirichlet';

    % set the input arguements: force the PML to be outside the computational
    % grid; switch off p0 smoothing within kspaceFirstOrder2D
    input_args = {'PMLInside', false, 'PMLSize', PML_size, 'PlotPML', false, 'Smooth', false};

    % run the simulation
    sensor_data(:,:,n) = permute(kspaceFirstOrder2D(kgrid, medium, source, sensor, input_args{:}),[2 1]);
end
sensor_data(isnan(sensor_data))=0;
Index exceeds the number of array elements (1).

Error in CPWC_linear_array_cyst (line 169)
disp('Launching kWave. This can take a while.');

Gather element signals

After calculaton we combine the signal recorded by the sensors according to the corresponding element

element_data=zeros(numel(kgrid.t_array),prb.N_elements,numel(seq));
for m=1:prb.N_elements
    if  ~isempty(element_sensor_index{m})
        element_data(:,m,:)=bsxfun(@times,sqrt(1./kgrid.t_array).',trapz(kgrid.y(source_pixels{m}),sensor_data(:,element_sensor_index{m},:),2));
    end
end

Band-pass filter

We remove some numerical noise by band-pass filtering

filtered_element_data=tools.band_pass(element_data,1/kgrid.dt,[0 1e6 8e6 10e6]);

Channel_data

We can now store the simulated data into a uff.channel_data class

channel_data = uff.channel_data();
channel_data.probe = prb;
channel_data.sequence = seq;
channel_data.initial_time = 0;
channel_data.sampling_frequency = 1/kgrid.dt;
channel_data.data = filtered_element_data;

% taking care of NaNs
channel_data.data(isnan(channel_data.data))=0;

Beamforming

To beamform we define a new (coarser) uff.linear_scan. We also define the processing pipeline and launch the beamformer

domain=uff.linear_scan('x_axis',linspace(domain.x_axis(1),domain.x_axis(end),512).',...
    'z_axis',linspace(domain.z_axis(1),domain.z_axis(end),512).');

pipe = pipeline();
pipe.channel_data = channel_data;
pipe.scan = domain;

pipe.receive_apodization.window = uff.window.hanning;
pipe.receive_apodization.f_number = F_number;

b=postprocess.coherent_compounding();
b.dimension = dimension.both;

das = pipe.go({midprocess.das b});
das.plot([],'DAS'); hold on;